﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections;
using System.ComponentModel;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Windows;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO.Packaging;
using System.Windows.Threading;

namespace MTS
{
    public enum ImageType { TERRAIN, HEIGHTMAP, OBLIQUE, ISOMETRIC, CAVEMAP, SPECTROGRAPH };
    public enum CacheType { SQLITE };
    
    

    public class WorldRenderer
    {
        BackgroundWorker worker = new BackgroundWorker();

        private long worldWidth;
        private long worldHeight;
        private long worldYOffset;
        private long worldXOffset;
        private int size;



        public WorldRenderer()
        {
           
        }

        private void recomputeWorldDimensions(List<LevelChunk> chunks)
        {
            // Recompute world size

            int minWorldx = int.MaxValue;
            int maxWorldx = int.MinValue;
            int minWorldy = int.MaxValue;
            int maxWorldy = int.MinValue;
            foreach (LevelChunk current in chunks)
            {
                minWorldx = Math.Min(minWorldx, (int)current.WorldX);
                maxWorldx = Math.Max(maxWorldx, (int)current.WorldX);
                minWorldy = Math.Min(minWorldy, (int)current.WorldY);
                maxWorldy = Math.Max(maxWorldy, (int)current.WorldY);
            }

            worldWidth = maxWorldx - minWorldx;
            worldHeight = maxWorldy - minWorldy;
            worldXOffset = minWorldx;
            worldYOffset = minWorldy;
#if DEBUG
            Console.Out.WriteLine("World is {0}x{1}, with offset of {2}x{3}", worldWidth, worldHeight, worldXOffset, worldYOffset);
#endif
        }

        public BitmapSource renderWorld(RenderSettings settings, BackgroundWorker worker)
        {

            List<String> regionFiles = this.getFolder(Path.Combine(settings.WorldPath, "region"), settings.Hell);
            List<LevelChunk> chunks = new List<LevelChunk>();

            string CachePath = Path.Combine(settings.CacheDir, "ChunkCache", settings.ToString());

            DateTime startTime = DateTime.Now;
            PixelFormat outputFormat = PixelFormats.Bgra32;
            int _bytesPerPixel = (outputFormat.BitsPerPixel + 7) / 8;
            settings.BytesPerPixel = _bytesPerPixel;
            settings.TypeOfCache = CacheType.SQLITE;

            int progress = 0;
            foreach (String regionPath in regionFiles)
            {
                //Create new LevelChunks
                Region newRegion = new Region(regionPath, settings);
                List<LevelChunk> newChunks = newRegion.getChunks();
#if DEBUG
                Console.Out.WriteLine("{0} chunks added", newChunks.Count);
#endif
                chunks.AddRange(newChunks);
                progress++;
                worker.ReportProgress((int)((float)progress / (float)(regionFiles.Count) * 100), "Initializing chunks");
                
                
            }
            // recalculate size
            recomputeWorldDimensions(chunks);
            chunks.Sort();
#if DEBUG
            Console.Out.WriteLine("World is {0}x{1}", worldWidth, worldHeight);
#endif
            // find tile dependancies for the render order, and put them in nice lists
            List<List<LevelChunk>> renderStrips = new List<List<LevelChunk>>();
            progress = 0;
            /*foreach (LevelChunk current in chunks)
            {
                // if the tile is not dependant on any existing tiles, put it in a new list
                bool done = false;
                foreach (List<LevelChunk> currentStrip in renderStrips)
                {
                    
                    foreach (LevelChunk other in currentStrip)
                    {
                        if (current.isDependant(other) || other.isDependant(current))
                        {
                            // check if other is already in a strip

                            if (currentStrip.Contains(other) && !done)
                            {
                                // if so, add current to this strip
                                currentStrip.Add(current);
                                done = true;
                                break;
                            }
                        }
                    }
                }
                if (!done)
                {
                    List<LevelChunk> newStrip = new List<LevelChunk>();
                    newStrip.Add(current);
                    renderStrips.Add(newStrip);
                }
                    // otherwise, make a new strip
                progress++;
                worker.ReportProgress((int)((float)progress / (float)(chunks.Count) * 100), "Sorting chunks");
            }*/
            long lastX = long.MinValue;
            foreach (LevelChunk current in chunks)
            {
                if (current.WorldX != lastX)
                {
                    // new strip
                    List<LevelChunk> newStrip = new List<LevelChunk>();
                    newStrip.Add(current);
                    renderStrips.Add(newStrip);
                    
                }
                else
                {
                    foreach (List<LevelChunk> currentStrip in renderStrips)
                    {
                        if (currentStrip.Count > 0)
                        {
                            if (currentStrip.ElementAt(0).WorldX.Equals(current.WorldX))
                            {
                                // add to this strip
                                currentStrip.Add(current);
                            }
                        }
                    }
                }
                lastX = current.WorldX;
                progress++;
                worker.ReportProgress((int)((float)progress / (float)(chunks.Count) * 100), "Sorting chunks");
            }
#if DEBUG
            Console.Out.WriteLine("{0} strips", renderStrips.Count);
#endif
            
            
            // recalculate world image size
            int worldImageWidth = ((int)worldWidth + 1);
            int worldImageHeight = ((int)worldHeight + 1);
            if (settings.TileType.Equals(ImageType.OBLIQUE))
            {
                worldImageWidth = worldImageWidth * 16;
                worldImageHeight = worldImageHeight * 16 + 129;
            }
            else if (settings.TileType.Equals(ImageType.TERRAIN) || settings.TileType.Equals(ImageType.HEIGHTMAP) || settings.TileType.Equals(ImageType.CAVEMAP) || settings.TileType.Equals(ImageType.SPECTROGRAPH))
            {
                worldImageWidth = worldImageWidth * 16;
                worldImageHeight = worldImageHeight * 16;
            }
            else
            {
                throw new Exception("Not yet implemented " + settings.TileType.ToString());
            }
#if DEBUG
            Console.Out.WriteLine("Image is {0}x{1}", worldImageWidth, worldImageHeight);
            Console.Out.WriteLine("trying to allocate {0} bytes for the image", worldImageWidth * worldImageHeight * _bytesPerPixel);
#endif
            // initialize the canvas
            //WriteableBitmap outputBitmap = new WriteableBitmap(worldImageWidth, worldImageHeight, 96, 96, outputFormat, null);
            byte[] outputImage = new byte[worldImageWidth * worldImageHeight * _bytesPerPixel];

            //sort the cache out
            using (ChunkCache cache = settings.getCache())
            {
                Console.WriteLine("Using cache file \"{0}\".", cache);
                progress = 0;
                int newChunks = 0;
                int oldChunks = 0;
                int _outerStride = worldImageWidth * _bytesPerPixel;
                
                Parallel.ForEach(renderStrips, currentStrip =>
                {
                    currentStrip.Sort();
                    foreach (LevelChunk current in currentStrip)
                    {
                        if (current.hasBlocks)
                        {
                            BlockColors colorer = new BlockColors();
                            current.Colorer = colorer;

                            TileImage currentTile = current.Image(cache);
                            // blit it
                            byte[] inputImageBytes = currentTile.Image;
                            int _innerStride = currentTile.Width * _bytesPerPixel;

                            Int32 destinationX = ((int)current.WorldX - (int)worldXOffset) * 16;
                            Int32 destinationY = ((int)current.WorldY - (int)worldYOffset) * 16;
                            Int32Rect destinationRect = new Int32Rect(destinationX, destinationY, currentTile.Width, currentTile.Height);
                            //get to the start of the new rectangle
                            int currentOffset = destinationRect.Y * _outerStride + destinationX * _bytesPerPixel;
                            for (int y = 0; y < currentTile.Height; y++)
                            {
                                for (int x = 0; x < currentTile.Width; x++)
                                {
                                    // Insert the pixel then move the backbuffer on a pixel
                                    int byteOffset = (x + y * currentTile.Width) * _bytesPerPixel;

                                    Color existing_color = Color.FromArgb(outputImage[currentOffset + 3],
                                                                            outputImage[currentOffset + 2],
                                                                            outputImage[currentOffset + 1],
                                                                            outputImage[currentOffset]);
                                    Color input_color = Color.FromArgb(inputImageBytes[byteOffset + 3],
                                                                                inputImageBytes[byteOffset + 2],
                                                                                inputImageBytes[byteOffset + 1],
                                                                                inputImageBytes[byteOffset]);
                                    if (existing_color.A == 0 && input_color.A > 0)
                                    {
                                        Color outputColor = this.blendColors(input_color, existing_color, 128);
                                        outputImage[currentOffset] = outputColor.B;
                                        outputImage[currentOffset + 1] = outputColor.G;
                                        outputImage[currentOffset + 2] = outputColor.R;
                                        outputImage[currentOffset + 3] = outputColor.A;
                                    }
                                    // move on a pixel
                                    currentOffset += _bytesPerPixel;
                                }
                                // move on a row
                                currentOffset += (_outerStride - currentTile.Width * _bytesPerPixel);
                            }
                            currentTile = null;


                            /*outputBitmap.Lock();
                            unsafe
                            {
                                int pBackBuffer = (int)outputBitmap.BackBuffer;
                                // Go to the first pixel from the global coordinates
                                pBackBuffer += destinationRect.Y * outputBitmap.BackBufferStride;
                                pBackBuffer += destinationRect.X * _bytesPerPixel;
                                // Now iterate over each pixel
                                for (int y = 0; y < currentTile.Height; y++)
                                {
                                    for (int x = 0; x < currentTile.Width; x++)
                                    {
                                        // Insert the pixel then move the backbuffer on a pixel
                                        int byteOffset = (x + y * currentTile.Width) * _bytesPerPixel;

                                        int existing_color_data = *((int*)pBackBuffer);
                                        Color existing_color = Color.FromArgb((byte)((existing_color_data & 0xFF000000) >> 24),
                                                                                (byte)((existing_color_data & 0x00FF0000) >> 16),
                                                                                (byte)((existing_color_data & 0x0000FF00) >> 8),
                                                                                (byte)((existing_color_data & 0x000000FF) >> 0));

                                        Color input_color = Color.FromArgb(inputImageBytes[byteOffset + 3],
                                                                            inputImageBytes[byteOffset + 2],
                                                                            inputImageBytes[byteOffset + 1],
                                                                            inputImageBytes[byteOffset]);

                                        if (existing_color.A == 0 && input_color.A > 0)
                                        {
                                            Color outputColor = this.blendColors(input_color, existing_color, 128);
                                            int color_data = outputColor.A << 24; // A
                                            color_data |= outputColor.R << 16; // R
                                            color_data |= outputColor.G << 8; // G
                                            color_data |= outputColor.B << 0; // B
                                            *((int*)pBackBuffer) = color_data;
                                        }
                                        pBackBuffer += _bytesPerPixel;
                                    }
                                    // Move on to the start of the next row (backbufferstride - width * bytes per pixel)
                                    pBackBuffer += (outputBitmap.BackBufferStride - currentTile.Width * _bytesPerPixel);
                                }
                            }
                            outputBitmap.AddDirtyRect(destinationRect);
                            outputBitmap.Unlock();*/
                            // report progress
                            progress++;
                            if (current.Cached)
                            {
                                oldChunks++;
                            }
                            else
                            {
                                newChunks++;
                            }
                            worker.ReportProgress((int)((float)progress / (float)(chunks.Count) * 100), "Processing chunks");
                        }

                    }
                });
                

                TimeSpan elapsed = DateTime.Now - startTime;
#if DEBUG
                Console.Out.WriteLine("Rendered {0} chunks in {1}", chunks.Count, elapsed);
                Console.Out.WriteLine("{0} new, {1} cached", newChunks, oldChunks);
#endif
                BitmapSource finalImage = BitmapSource.Create(worldImageWidth, worldImageHeight, 96.0, 96.0, outputFormat, null, outputImage, _outerStride);
                finalImage.Freeze();
                return finalImage;
            }
            
        }

        private List<String> getFolder(String folder, bool hell)
        {
            List<String> files = new List<String>();
            foreach (String directory in Directory.GetDirectories(folder))
            {
                if (hell && directory.Substring(directory.LastIndexOf('\\') + 1).StartsWith("DIM-"))
                {
                    files.AddRange(getFolder(directory, hell));
                }
                else if (!hell && !directory.Substring(directory.LastIndexOf('\\') + 1).StartsWith("DIM-"))
                {
                    files.AddRange(getFolder(directory, hell));
                }
            }
            foreach (String fileItem in Directory.GetFiles(folder, "*.mcr"))
            {
                files.Add(fileItem);
            }
            return files;
        }

        private Color blendColors(Color A, Color B, int h)
        {
            Color C = new Color();
            float Aa = A.ScA;
            float Ba = B.ScA;
            float Alpha = Aa + Ba * (1 - Aa);
            C.R = (byte)((float)A.R * Aa + (float)B.R * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.G = (byte)(float)(A.G * Aa + (float)B.G * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.B = (byte)((float)A.B * Aa + (float)B.B * Ba * (1 - Aa) * (float)h / 128 / Alpha);
            C.A = (byte)(Alpha * 255);
            return C;
        }
    }
}
